use std::{
    io::{self, stdout, Stdout, Write},
    thread::sleep,
    time::Duration,
};

use crossterm::{
    cursor::{Hide, MoveTo, Show},
    event::{poll, read, Event, KeyCode},
    style::Print,
    terminal::{disable_raw_mode, enable_raw_mode, size, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
    QueueableCommand,
};
use tetris_core::{game::{self, Game}, input::{Input, InputMap}, tetris::Model, vec2, Board, Tetris};

fn main() -> io::Result<()> {
    enable_raw_mode()?; // P.S.

    stdout().queue(EnterAlternateScreen)?.queue(Hide)?.flush()?; // P.S.
    let mut size = size()?; // P.S.

    let t = Model::new(1, [vec2(-1, 0), vec2(0, 0), vec2(1, 0), vec2(0, 1)]);
    let o = Model::new(2, [vec2(1, 0), vec2(0, 0), vec2(0, 1), vec2(1, 1)]);
    let l = Model::new(3, [vec2(0, 0), vec2(1, 0), vec2(0, 2), vec2(0, 1)]);
    let j = Model::new(4, [vec2(0, 0), vec2(-1, 0), vec2(0, 1), vec2(0, 2)]);
    let i = Model::new(5, [vec2(0, 0), vec2(0, -1), vec2(0, 1), vec2(0, 2)]);
    
    let models = vec![t, o, l, j, i];
    let mut game = Game::new(&models, (20, size.1 as _));

    let mut inputs = Input::new();

    'main: loop {
        // handle inputs P.S.
        collect_inputs(&mut inputs, &mut size)?;

        if let game::Ret::Exit = game.tick(&mut inputs, 50) {
            break 'main;
        }
        // render P.S.

        let mut pass = stdout();
        pass.queue(Clear(ClearType::All))?;

        draw_board(&mut pass, &size, game.board());
        draw_tetris(&mut pass, &size, game.tetris());
        draw_next(&mut pass, game.next_model());

        pass.flush()?;

        sleep(Duration::from_millis(50))
    }

    stdout()
        .queue(Clear(ClearType::All))?
        .queue(MoveTo(0, 0))?
        .queue(Show)?
        .queue(LeaveAlternateScreen)?
        .flush()?;
    disable_raw_mode()?;
    Ok(())
}

fn val_to_char(val: u8) -> char {
    match val {
        1 => '@',
        2 => '#',
        3 => 'H',
        4 => 'K',
        5 => 'O',
        6 => 'G',
        _ => ' ',
    }
}

fn draw_board(pass: &mut Stdout, size: &(u16, u16), board: &Board) {
    for i in 0..size.1 {
        pass.queue(MoveTo(board.width() as _, i)).unwrap();
        pass.queue(Print('|')).unwrap();
    }
    board.visit(|x, y, v| {
        if v == 0 {
            return;
        }
        pass.queue(MoveTo(x as u16, size.1 - 1 - y as u16)).unwrap();
        pass.queue(Print(val_to_char(v))).unwrap();
    })
}

fn draw_tetris(pass: &mut Stdout, size: &(u16, u16), tetris: &Tetris) {
    tetris.visit(|x, y, val| {
        if (0..size.0 as i32).contains(&x) && (0..size.1 as i32).contains(&(size.1 as i32 - 1 - y)) {
            pass.queue(MoveTo(x as u16, (size.1 as i32 - 1 - y) as u16))
                .unwrap();
            pass.queue(Print(val_to_char(val))).unwrap();
        }
        true
    });
}

fn draw_next(pass: &mut Stdout, model: &Model) {
    pass.queue(MoveTo(22, 2)).unwrap();
    pass.queue(Print("NEXT:")).unwrap();
    for block in model.blocks.iter() {
        pass.queue(MoveTo((25 + block.x) as u16, (5 + block.y) as u16)).unwrap();
        pass.queue(Print(val_to_char(model.val))).unwrap();
    }
}

fn collect_inputs(inputs: &mut Input<InputMap>, size: &mut (u16, u16)) -> io::Result<()> {
    while poll(Duration::from_micros(1))? {
        match read()? {
            Event::Key(key) => match key.code {
                KeyCode::Up => inputs.push_event(InputMap::Rotate),
                KeyCode::Esc => inputs.push_event(InputMap::Exit),
                KeyCode::Left => inputs.push_event(InputMap::MoveLeft),
                KeyCode::Right => inputs.push_event(InputMap::MoveRight),
                KeyCode::Down => inputs.push_event(InputMap::MoveDown),
                KeyCode::Char(ch) => match ch.to_ascii_lowercase() {
                    'w' => inputs.push_event(InputMap::Rotate),
                    'a' => inputs.push_event(InputMap::MoveLeft),
                    'd' => inputs.push_event(InputMap::MoveRight),
                    's' => inputs.push_event(InputMap::MoveDown),
                    _ => (),
                },
                _ => (),
            },
            Event::Resize(w, h) => *size = (w, h),
            _ => (),
        }
    }
    Ok(())
}
